use std::io::{Read, Write, Take, self, Seek, SeekFrom};
use std::net::{TcpListener, ToSocketAddrs};
use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
use std::fs::{self, File};

use native_tls::{Identity, TlsAcceptor};

use crate::util::{StreamWrapper, self};
use crate::constant::*;

enum Error {
    NetErr,
    ClientErr(String),
    SysErr(Box<dyn std::error::Error>)
}

impl<T: std::error::Error + 'static> From<T> for Error {
    fn from(e: T) -> Self {
        Self::SysErr(Box::new(e))
    }
}

impl From<util::StreamError> for Error {
    fn from(_: util::StreamError) -> Self {
        Self::NetErr
    }
}

struct User {
    user_path: PathBuf,
    upload_path: PathBuf,
    auth_path: PathBuf
}

impl User {
    fn create(base: &str, username: &str) -> Result<Self, Error> {
        if !util::is_valid_file_name(username) {
            return Err(Error::ClientErr("Invalid username".to_owned()));
        }

        let mut user_path = PathBuf::from(base);
        user_path.push(username);

        let mut upload_path = user_path.clone();
        upload_path.push("upload");

        let mut auth_path = user_path.clone();
        auth_path.push("auth");

        Ok(User { user_path, upload_path, auth_path })
    }

    fn create_files(&self, auth: &[u8]) -> Result<(), std::io::Error> {
        fs::create_dir(&self.user_path)?;
        fs::create_dir(&self.upload_path)?;
        fs::write(&self.auth_path, auth)?;
        Ok(())
    }

    fn register(&self, password: &str) -> Result<bool, Error> {
        if self.user_path.is_dir() {
            return Ok(false);
        }

        let password_hash = util::create_password_hash(password.as_bytes())?;

        self.create_files(&password_hash)?;

        Ok(true)
    }
    
    fn login(&self, password: &str) -> Result<bool, Error> {
        if !self.user_path.is_dir() {
            return Ok(false)
        }

        let password_hash = fs::read(&self.auth_path)?;

        Ok(util::check_password_hash(password.as_bytes(), &password_hash))
    }

    fn list_files(&self, part: bool) -> Result<Vec<(u64, String)>, Error> {
        Ok(fs::read_dir(&self.upload_path)?.filter_map(|e| {
            if let Ok(entry) = e {
                if let Some(name) = entry.file_name().to_str() {
                    if part && name.ends_with(".part") || !part && name.ends_with(".file") {
                        if let Ok(meta) = entry.metadata() {
                            return Some((meta.len(), name[..name.len() - 5].to_owned()));
                        }
                    }
                }
            }
            None
        }).collect())
    }

    fn begin_upload(&self, name: &str) -> Result<(), Error> {
        if !util::is_valid_file_name(name) {
            return Err(Error::ClientErr("Invalid file name".to_owned()));
        }

        let mut file_path = self.upload_path.clone();
        file_path.push(format!("{name}.file"));
        if file_path.is_file() {
            return Err(Error::ClientErr("Full file exists".to_owned()))
        }

        let mut path = self.upload_path.clone();
        path.push(format!("{name}.part"));
        if path.is_file() {
            return Err(Error::ClientErr("Uploading file exists".to_owned()))
        }

        File::create(path)?;

        Ok(())
    }

    fn upload(&self, name: &str, offset: u64, take: &mut Take<impl Read>) -> Result<(), Error> {
        if !util::is_valid_file_name(name) {
            return Err(Error::ClientErr("Invalid file name".to_owned()));
        }

        let mut path = self.upload_path.clone();
        path.push(format!("{name}.part"));
        if !path.is_file() {
            return Err(Error::ClientErr("File does not exist".to_owned()));
        }

        let mut file = File::options().append(true).open(path)?;

        let len = file.metadata()?.len();
        if len != offset {
            return Err(Error::ClientErr(format!("Invalid offset: expected {len}, got {offset}")));
        }

        io::copy(take, &mut file)?;

        if take.limit() != 0 {
            return Err(Error::NetErr);
        }

        Ok(())
    }

    fn end_upload(&self, name: &str, hash: &[u8]) -> Result<bool, Error> {
        if !util::is_valid_file_name(name) {
            return Err(Error::ClientErr("Invalid file name".to_owned()));
        }

        let mut path = self.upload_path.clone();
        path.push(format!("{name}.part"));
        if !path.is_file() {
            return Err(Error::ClientErr("File does not exist".to_owned()));
        }

        if util::file_hash(&path)? != hash {
            return Ok(false);
        }

        let mut file_path = self.upload_path.clone();
        file_path.push(format!("{name}.file"));
        fs::rename(&path, file_path)?;

        Ok(true)
    }

    fn download(&self, name: &str, offset: u64, len: u64, writer: &mut impl Write) -> Result<(), Error> {
        if !util::is_valid_file_name(name) {
            return Err(Error::ClientErr("Invalid file name".to_owned()));
        }

        let mut path = self.upload_path.clone();
        path.push(format!("{name}.file"));
        if !path.is_file() {
            return Err(Error::ClientErr("File does not exist".to_owned()));
        }

        let mut file = File::open(path)?;
        let file_len = file.metadata()?.len();
        if offset + len > file_len {
            return Err(Error::ClientErr(format!("Invalid offset or len: offset ({}) + len ({}) = {} > file size ({})", offset, len, offset + len, file_len)));
        }

        file.seek(SeekFrom::Start(offset))?;
        let mut take = file.take(len);
        io::copy(&mut take, writer)?;

        if take.limit() != 0 {
            return Err(Error::NetErr);
        }

        Ok(())
    }

    fn get_hash(&self, name: &str) -> Result<Vec<u8>, Error> {
        if !util::is_valid_file_name(name) {
            return Err(Error::ClientErr("Invalid file name".to_owned()));
        }

        let mut path = self.upload_path.clone();
        path.push(format!("{name}.file"));
        if !path.is_file() {
            return Err(Error::ClientErr("File does not exist".to_owned()));
        }

        Ok(util::file_hash(&path)?)
    }
}

fn load_identity(crt_path: &str, key_path: &str) -> Result<Identity, Box<dyn std::error::Error>> {
    let crt_buf = fs::read(crt_path)?;

    let key_buf = fs::read(key_path)?;

    Ok(Identity::from_pkcs8(&crt_buf, &key_buf)?)
}

fn handle_connection<T: Read + Write>(stream: &mut StreamWrapper<T>) -> Result<(), Error> {
    let base_dir = "../data";

    let auth_cmd = stream.recv_u8()?;
    let username = stream.recv_str8()?;
    let user = User::create(base_dir, &username)?;

    let auth_ok = match auth_cmd {
        CMD_AUTH_REG => {
            let password = stream.recv_str8()?;
            user.register(&password)?
        },
        CMD_AUTH_LOGIN => {
            let password = stream.recv_str8()?;
            user.login(&password)?
        },
        cmd => return Err(Error::ClientErr(format!("Invalid auth command: {cmd}")))
    };

    if auth_ok {
        stream.send_u8(1)?;
    } else {
        stream.send_u8(0)?;
        return Ok(());
    }

    while let Ok(cmd) = stream.recv_u8() {
        match cmd {
            CMD_LIST_FINISHED | CMD_LIST_UPLOADING => {
                let list = user.list_files(cmd == CMD_LIST_UPLOADING)?;
                stream.send_u64(list.len().try_into().unwrap())?;
                for file in user.list_files(cmd == CMD_LIST_UPLOADING)? {
                    stream.send_u64(file.0)?;
                    stream.send_buf8(file.1.as_bytes())?;
                }
            },
            CMD_BEGIN_UPLOAD => user.begin_upload(&stream.recv_str8()?)?,
            CMD_UPLOAD => {
                let name = stream.recv_str8()?;
                let offset = stream.recv_u64()?;
                let len = stream.recv_u64()?;
                let mut take = Read::by_ref(&mut stream.0).take(len);
                user.upload(&name, offset, &mut take)?;
            },
            CMD_END_UPLOAD => {
                let name = stream.recv_str8()?;
                let mut hash = [0; 32];
                stream.recv(&mut hash)?;
                let ok = user.end_upload(&name, &hash)?;
                stream.send_u8(if ok { 1 } else { 0 })?;
            },
            CMD_DOWNLOAD => {
                let name = stream.recv_str8()?;
                let offset = stream.recv_u64()?;
                let len = stream.recv_u64()?;
                user.download(&name, offset, len, &mut stream.0)?;
            },
            CMD_GET_HASH => {
                let name = stream.recv_str8()?;
                let hash = user.get_hash(&name)?;
                stream.send(&hash)?;
            },
            cmd => return Err(Error::ClientErr(format!("Invalid command: {cmd}")))
        }
    }

    Ok(())
}

pub fn run_server(addr: impl ToSocketAddrs, crt_path: &str, key_path: &str) -> Result<(), String> {
    let identity = load_identity(crt_path, key_path)
        .map_err(|e| format!("Failed to load identity: {e}"))?;

    let listener = TcpListener::bind(addr)
        .map_err(|e| format!("Failed to listen: {e}"))?;

    let acceptor = TlsAcceptor::new(identity)
        .map_err(|e| format!("Failed to create acceptor: {e}"))?;
    let acceptor = Arc::new(acceptor);

    for stream in listener.incoming() {
        let stream = stream.map_err(|e| format!("Failed to accept: {e}"))?;

        let acceptor = acceptor.clone();

        thread::spawn(move || {
            if let Ok(stream) = acceptor.accept(stream) {
                let mut wrapper = StreamWrapper(stream);
                if let Err(e) = handle_connection(&mut wrapper) {
                    match e {
                        Error::SysErr(err) => {
                            eprintln!("System error: {err}");
                        },
                        Error::ClientErr(err) => {
                            eprintln!("Client error: {err}");
                        },
                        Error::NetErr => {
                            eprintln!("Disconnected unexpectedly")
                        }
                    }
                } else {
                    eprintln!("Disconnected")
                }
            } else {
                eprintln!("TLS handshake error");
            }
        });
    }

    Ok(())
}
